DBA致命的低级工作?超大型系统数据库版本质量控制
Editor's Note
The following article is from dbaplus社群 Author 宇文湛泉
作者介绍
宇文湛泉,现任金融行业核心业务系统DBA,主要涉及Oracle、DB2、Cassandra、MySQL、GoldenDB、TiDB等数据库开发工作。
这似乎是DBA工作中最为无聊、繁琐以及最没有技术含量的事情。这也许是DBA工作中,最可能一击致命、产生巨大损失的工作内容。这部分内容虽然LOW,但是对于DBA工作来说,却是基石般的存在。
从最朴素的角度上看,投产上线的数据库版本只有两部分,一部分是DDL文件,另一部分是在OS上运行生效DDL文件的脚本。当一个系统里的数据库表并不多的时候,数据库版本可以作为应用程序版本的一部分。数据库建表、变更,由开发者或者运维者执行生效到生产环境即可。往往有经验的开发者或者架构师,可以凭借丰富的经验,一眼看出版本生效是否存在风险并给出回避方案。
当系统越做越大,变得复杂起来,数据库表变得种类繁多,开发者的人数也越来越多。通常会有出现两种选择。一种是,谁的表谁管,即每次投产的数据库变更归对应的开发者,依然视为应用程序版本的一部分。另一种是,整个系统的所有数据库变更,由某一个DBA来统一实施。
前一种方式,往往会产生各式各样,五花八门的实施方案。因为每个程序员都有自己的开发习惯和喜欢用的脚本。如果这个系统,不仅复杂,而且还很重要。那么这个系统的架构师就不得不花加倍的时间,来复核这些五花八门的方案的正确性和风险程度,以确保投产上线的万无一失。
这时候,另一种模式会有个显著的好处,因为它有一个统一的实施工艺,这对于系统架构师或许会非常的重要。因为无论这个系统里面有多少数据库表,架构师只需要审核有限种类的实施方法就行了。毕竟只有一个实施的DBA,怎么着也不可能做出太多的实施方案。不过显而易见,这种模式对这个实施的DBA可不是什么友好的事情。我们来想象一下,若是一个DBA做一个超大规模的数据库版本,可能会遇到什么?
一、版本准备阶段
版本准备阶段,这指的是上线范围确认之后,到上线投产之前,做准备的阶段。
1、手滑风险
如果一次投产,仅仅是新建一张表或者加一个索引。选择手搓一个SQL语句并拍到环境里的情况,并不罕见。甚至不能说这有什么非常不合理。但是手写数据库DDL和生效脚本绝对是充满了风险和不确定性的一件事情。也许在测试环境或者专门的版本验证环境运行一次版本,能有效的挡住一些错误,但是绝对不会是万无一失的。
举个例子:比如说数据库表的最后一个RANGE的范围(000-999)手滑写成了(000-998),凑巧测试环境,并没有这条边界数据,那么这个问题就极有可能在生产报错时能才能发现。一旦一次变更的内容足够多,那手工版本不出错的可能性非常的低。想象一下,你面对100个手写DDL文件时,你从第1个检查开始检查,在看第100个文件的时候,你是否还保持着你可靠的专注力,以确保你不会犯低级错误。
2、版本范围准确性
当你面对很多的开发人员,在某一次版本,一次性变更100张甚至更多数据库表时,你做完数据库版本,难免会产生一种自我怀疑,“我有没有可能漏了一个索引?”更要命的是,尽管这100个表当初都是你亲自做的数据库设计,评审的SQL语句。但是经过测试过程中,解决问题、调整方案。这时候其中的某一张表,到底有几个索引,到底是什么使用场景,你可能都记不清楚了。这时候如何对整个版本的正确性有把握呢?
3、版本内容准确性
当上线前,你打开上百个DDL文件,任何一张表都可能就有几十个字段加若干索引。假设你是一个记忆力不错的DBA,你几乎记住了每一张表的各种使用形态,清楚它的分区等存储设计,了解哪些SQL使用哪个索引,甚至于你记得针对业务场景设计了一些特别参数,这些已经相当不易。
如果说开发的程序员,在测试过程中,调整了某一两个字段的属性。这时候还要让一个超大系统的DBA,把这个细粒度级别的内容,都记得清清楚楚,是非常困难的。所以,超大型的数据库版本,必然会需要核准机制来确认你上线的DDL是完全没有问题的。最简单的方式可能是,DBA需要确认投产使用的DDL于开发环境和各种功能、非功能测试环境的DDL一致。或者DBA根本就是用同一个DDL文件来实施所有的环境的。
4、警示机制
对于特别重要的数据库表,或者影响面特别重要的交易,在投产准备的过程中,应该要有足够的警示和应急预案。比如说在线DDL有可能导致线上交易卡顿。当版本中某一个数据库表涉及到类似监管机构考核响应时长的交易时,在版本实施之前,DBA应该提前给出相关的警示。
二、实施阶段
实施阶段,这指版本实施的窗口时间段。
1、生效方式和流程
如果你要创建三张表,你有可能会写三个脚本依次运行,也有可能会写一个包含创建三张表的脚本一次性运行。这看起来好像没什么区别。但是在大型版本的情况下,是完全不一样的。我们先简单的增加一下工作量,假设我们一个稍微复杂一点的数据库表变更表中字段,我们大约需要以下几个步骤:
数据导出 数据加工 数据库表删除 数据库表重建、索引重建 数据加载
只要同时操作20张数据库表,就有100个小步骤。如果每一个小步骤需要执行一个脚本,那么投产切换的步骤就需要执行100个脚本。这时候DBA基本上就不太可能选择自己去手工运行这100个脚本了。那用一个脚本运行这100个DDL行不行?大概率也是不行的,因为在7*24的系统上,你可能要在有限的时间窗口里,完成大量的步骤,这大概率需要更合理的流程。
比如说数据库表的导出、不同数据库表的数据加工等操作可以同步并行执行。这时候,就需要考虑不同步骤之间的依赖关系。随着多种变更方式混合在一起,可能有的项目新建表,有的是加索引等等,超大型的版本流程会变得复杂起来,而且不易于人类来记忆。这时候,DBA需要设计一个流程调度者,它可能是某个程序进程,来有条不紊的控制整体版本流程的高效生效。
2、异常报错处置
即使在拥有一个完美的生效流程的前提下,也无法保证版本实施生效过程中,绝对不报错。比如说,你的用户缺少某个权限、数据加工中磁盘空间不足等等。这时候你设计的调度程序,需要能够准确的识别异常,并可以隔离掉与这个步骤有关联的步骤,同时保证与之无关的步骤还能继续执行。
此外,在问题解决之后,异常相关的步骤还需要比较简单的重新唤起机制。整个异常处理的过程,需要清晰的日志痕迹,方便快速的错误定位和事后复盘。
3、检核机制
即使你建了100张表的超大流程完美运行,没有任何报错。你会不会产生一种怀疑,“我有没有可能漏跑一个索引?”难不成执行完之后,去一个个DESC表来检查运行结果?显然不可能。所以超大型的数据库版本一定会要配备生效后的检核机制,它可能是针对数据或者是元数据的。
三、进阶问题
除了这些之外,既然你是一个超大型系统的DBA,你所面对的肯定不仅仅是如此,当然接下面这些问题的本质与上面的问题还是一致的。
1、版本叠加
一个超大型系统,在一些公共或者平台能力的数据库表的修改上会有版本叠加的可能性,比如说程序员M,因为某个需求在某个表A上增加了字段COL1。同时程序员N,因为另一个需求在表A上增加了字段COL2。他们有可能在同一天,或者是前后差几天的时间里面上线。M和N甚至可能在不同的环境开发和测试,他们有可能互相不知道对方需求的存在。
这时候,DBA需要在很早的时候,甚至是他们开发的早期,就具有识别版本可能叠加风险的能力。而不同项目组,使用了不同的测试环境,DBA还需要确保上线的数据库版本,在多个不同测试数据库版本同时存在时,依旧准确无误。
2、量级性异常
系统一大了,奇怪的错误也更容易出现。因为很多时候,往往由于成本原因,超大型系统往往难以配备和生产环境一模一样的硬件设备。减少测试环境的数据量,是非常常见的作法。那么测试环境的数据量远少于生产环境时,就经常会因此出现问题。比如磁盘空间不足、磁盘吞吐能力瓶颈、执行计划差异等等。还有可能是大规模的数据清理或者操作,引起诸如REDO 空间不足,BINLOG同步炸掉之类的问题。
四、我们怎么应对
几乎每一个程序员,都会写一些自己常用的工具代码来简化自己的日常工作。当然也包括DBA。这样做的好处是显而易见的,我们看看可以通过一些工具程序来做些什么:
工具程序会根据开发者的使用数据结构(或者企业级元数据字典),直接产生DDL,这可以非常有效的避免手滑问题。最重要的是,产生一个DDL文件和产生一万个DDL文件,对于代码来说,不仅仅花费时间几乎是一样,出现问题的概率几乎是一样的。
工具程序在数据库评审的时候,就会根据项目录入非常具体的设计信息和范围,包括涉及的表、索引有哪些,SQL语句有哪些。并且在每一次设计调整时,我们都可以通过工具生效在测试环境之后,留下变化的记录。那么,在某个投产日的项目范围确定之后,整个系统的数据库变更范围随之确定。这样不容易上错内容,也不容易缺少什么内容。
工具会根据不同的变更类型,使用标准化的流程。比如工具检测到,投产内容是表的末尾增加字段,工具即产生对应的ALTER TABLE语句和运行脚本。再大再复杂的数据库版本,它的生效流程种类总归是有限的(当然也可以在一定的时间里,工具系统逐步完善和迭代)。程序总是能冷静和准确的根据我们编码出来的规则,选择恰当生效方式和流程。系统架构师甚至都不用每一次上线都亲自来复核版本是否靠谱,他只需要在这一套工具程序稳定运行之前,复核规则和代码是否可靠即可。几乎是一劳永逸。
异常处理往往是需要人工介入的,比如你加了磁盘、赋予了权限。但是异常处理完的后续工作,最好还是交回工具程序来处理。这里就需要程序具有断点续跑和重跑的能力。工具程序需要清楚的识别哪些步骤执行了,哪些没有执行,这比起DBA在万千作业里面去判断要重跑哪些脚本,先跑哪个再跑哪个要靠谱得多。
投产检核其实通常并没有什么特别复杂内容,版本末端的检核程序可以比人类检核更加准确、更加高效。
除此之外,最最重要的部分是这件事情。任何工具、流程可能确实无法达到完美的程度,但是它却可以被固化下来,它是可持续积累的,这是它最大优势。有一种情况是非常常见,就是同一个错误会反复发生。某一个错误在发生之初,可能会被程序员重视,并在接下来的几次投产过程中被反复检查。
但是,往往过了几年之后,同样的错误非常有可能再次发生。因为可能这中间的人力发生了变化。即使是同一个人,他也可能因为忘了,导致错误的重复发生。而针对某一个问题的代码,一旦被写到了工具程序中,这个问题再发生的概率近似为0。
五、我们的思路和原则
以上我们基本上只说了,我们需要做什么,并没有说具体是怎么实现的。我们实现过程中的几个原则是比较重要的。
1、零手工原则
这个原则即上线使用的所有DDL文件,执行脚本,均100%由版本准备工具程序产生,不允许手工编写。整个版本的范围,输入信息应该仅仅是DBA选择某次版本日上线的哪些项目。这些项目最终应该关联多少DDL,并产生什么样的生效脚本,均应该由程序生成。
2、一眼看清原则
大型的系统,在投产前,因为各种各样的因素,调整上线范围是经常发生的。所以,有时候还会需要DBA会去,检核程序生成的代码文件。需要人眼看的部分,一定要非常的清晰。切莫把生效用的脚本代码和投产范围相关表名、库名之类的内容,在文件内混在一起。要进行清晰地封装。
在上线版本包里,应该比较清晰的分为运行代码部分和上线范围部分。
运行代码文件中,通常写的是标准化的流程和程序运行的代码,这部分一旦稳定之后,每次版本上线时,应该是固定不变的。另一部分,是每次上线都不一样的部分,比如表清单等跟上线范围相关的内容,简明易读。此外在版本生效过程中,输出的错误日志,也需要非常清晰,以方便DBA去查看。这里也需要将流程的颗粒设计得非常细,在错误产生得时候,就可以快速精确的定位错误发生点。
3、傻瓜原则
在正常情况下,上线的操作步骤一定要足够简化。如果系统没有异常状况,应该是一个傻子都可以完成版本的正确生效。这个原则主要用于排除人为主观因素,对投产带来的风险。把复杂的事情,留在投产之前。上线的时候,不依赖于当时DBA的状态和反应速度。我们会把整个版本生效简化到运行一个入口脚本。异常处置时,我们也还是重新运行这同一个脚本。极致简化,不给操作者犯错的空间。
不得不说,数据库版本质量控制,是DBA工作中我最不喜欢的部分。但是确实我最花心思的部分。上面的这些内容,无论思路或者程序的实现,其实并没有很难。但是一个大型系统的DBA,对于每一次上线的数据库版本,心理有把握很难。有100%正确把握,就非常非常难。
我们可能需要几年,甚至于整个系统运行的周期,都在不断迭代,修补我们的工具程序,来堵住各种意想不到的漏洞,直到我们的版本质量,越来越接近完美。
Bytebase 2.6.0 - 支持通过 LDAP 配置 SSO,支持 RisingWave 数据库
PlanetScale vs. Neon - MySQL 和 Postgres 间的第二仗